Thursday, March 26, 2009

Expand/Collapse Rows Dynamically with DOM and JavaScript

Suppose, you have a table structured as follows:



The first line for a new month is always a total of the project amounts. This structure comes from an sql query, using the Oracle GROUP BY CUBE clause. Would not it be nice to be able to see only the rows with intermediate totals, expanding this or that month if necessary.



Here is a set of JavaScript functions that collapses and expands table rows:

var tbl;

function getParent(el, pTagName) {
if (el == null) return null;
else if (el.nodeType == 1 && el.tagName.toLowerCase() == pTagName.toLowerCase())
return el;
else
return getParent(el.parentNode, pTagName);
}


function toggleSection(lnk){

var td = lnk.parentNode;
var table = getParent(td,'TABLE');
var len = table.rows.length;
var tr = getParent(td, 'tr');
var rowIndex = tr.rowIndex;
var rowHead=table.rows[rowIndex].cells[1].innerHTML;

lnk.innerHTML =(lnk.innerHTML == "+")?"-":"+";
vStyle =(tbl.rows[rowIndex+1].style.display=='none')?'':'none';

for(var i = rowIndex+1; i < len;i++){
if (table.rows[i].cells[1].innerHTML==rowHead){
table.rows[i].style.display= vStyle;
table.rows[i].cells[1].style.visibility="hidden";
}
}
}

function collapseRows(){
tables =document.getElementsByTagName("table");
for(i =0; i < tables.length;i++){
if(tables[i].className.indexOf("expandable") != -1)
 tbl =tables[i];
}

if(typeof tbl=='undefined'){
 alert("Could not find a table of expandable class");
 return;
}

//assume the first row is headings and the first column is empty
var len = tbl.rows.length;
var link = '<a href="#" onclick="toggleSection(this);return false;">+ </a> ';

var rowHead = tbl.rows[1].cells[1].innerHTML;

for (j=1; j < len;j++){
 //check the value in each row of column 2
 var m = tbl.rows[j].cells[1].innerHTML;

 if(m!=rowHead || j==1){
  rowHead=m;
  tbl.rows[j].cells[0].innerHTML = link;
  tbl.rows[j].style.background = "#CCFF99";
 } 
 else
  tbl.rows[j].style.display = "none";
}
}

window.onload=collapseRows;

Let me briefly explain what the script does. When a page has loaded, the collapseRows function is called. It looks for a table with a class named 'expandable'. If such a table is found, a reference to the table is stored in a variable tbl, and the number of rows in the table is stored in a variable len.

var len = tbl.rows.length;

Then, we create a variable that holds an html link that will call the toggleSection function when linked. The function parameter will contain a reference to the link itself. We using the this keyword here.
var link = '<a href="#" onclick="toggleSection(this);return false;">+ </a> ';

The variable rowHead in the next statement will contain the text in the second column of the first row(not counting the table head). In this table it is January.

var rowHead = tbl.rows[1].cells[1].innerHTML;

The next piece of the code loops through each row, and if the text in the second column contains repeating information, it hides that row. Also, it assigns a different color to the first row with non-duplicate text.
for (j=1; j < len;j++){
 var m = tbl.rows[j].cells[1].innerHTML;
 if(m!=rowHead || j==1){
  rowHead=m;
  tbl.rows[j].cells[0].innerHTML = link;
  tbl.rows[j].style.background = "#CCFF99";
 }
 else
  tbl.rows[j].style.display = "none";
}

The next function, toggleSection, hides or collapses rows with duplicate months. As a parameter, the function accepts a reference to the link that calls the function. Using the link reference, we get a reference to the containing table cell, row, and then the row index for the cell:

var td = lnk.parentNode;
var tr = getParent(td, 'tr');
var rowIndex = tr.rowIndex;

We obtain text in the second column of that row (January or February, in this example):

var rowHead=table.rows[rowIndex].cells[1].innerHTML;

Next, we switch the text of the link from plus to minus or vice versa.

lnk.innerHTML =(lnk.innerHTML == "+")?"-":"+";

Then, we prepare a style to be used for the next rows.

vStyle =(tbl.rows[rowIndex+1].style.display=='none')?'':'none';

Then, we loop through the table, and if the text in the second column of each row is the same as the text in the same column of the row that contains the link, we hide or show that row depending on its initial state.

for(var i = rowIndex+1; i < len;i++){
if (table.rows[i].cells[1].innerHTML==rowHead){
 table.rows[i].style.display= vStyle;
}

We also hide the repeating text in the second column:

table.rows[i].cells[1].style.visibility="hidden";

Don't forget, the table must contain an empty first column to hold a plus or minus sign and have a class named 'expandable'.

See a demo here.

The script can be downloaded here.

Another post dealing with the HTML table :
Highlight Table Cells with JavaScript



 
 
 

7 comments:

  1. >> and have an id named 'expandable'.

    I think you mean "class named 'expandable'" because in your code you have:

    if(tables[i].className.indexOf("expandable") != -1)

    ReplyDelete
  2. Thanks for the catch. I'll correct the text.

    ReplyDelete
  3. Is there an easy way to add an additional level to this script? I have this working as is, but I need to add a subgroup to expand/collapse rows for, I'm a javascript beginner so not clear on if and how this would be done. Thank you.

    ReplyDelete
  4. This is an awesome script! I appreciate you sharing it. Is there a way to allow the expand/collapse of all rows? It would be great if there was a toggle link for this on the header row.

    ReplyDelete
  5. This comment has been removed by the author.

    ReplyDelete
  6. Sure. To add a toggle link on the header, add the following lines at the end of the toggleRows function:

    var headerLink ='<a href="#" onclick="toggleTable(this);return false;">+</a>';
    tbl.rows[0].cells[0].innerHTML = headerLink;

    Here is the new function:

    function toggleTable(lnk){

    var td = lnk.parentNode;
    var table = getParent(td,'TABLE');
    var len = table.rows.length;
    var vDisplay = (lnk.innerHTML == "+")?'':'none';

    for(var i = 1; i < len;i++){
    if(table.rows[i].cells[2].innerHTML.trim() =='')
    table.rows[i].style.display= '';
    else
    table.rows[i].style.display= vDisplay;
    }
    lnk.innerHTML =(lnk.innerHTML == "+")?"-":"+";
    }

    ReplyDelete
  7. Thanks for the help, and code update. However, I'm having a little trouble getting the new code to work. The "expand all" doesn't change the + to - for all, and messes them up a bit. Also, the "collapse all" doesn't appear to work correctly. You can see what I'm talking about here: http://jsbin.com/woleyedoqe/1/edit

    ReplyDelete